package controller

import (
	"encoding/json"
	"io"
	"io/ioutil"
	"os"
	"path/filepath"
	"recorder/internal"
	"recorder/log"
	"strings"
	"time"
)

type JobFile struct {
	Job      *internal.Job
	FilePath string
}

type Scanner struct {
	cfg   *internal.Config
	jobs  map[string]*JobFile
	paths map[string]*JobFile
	done  chan struct{}
	C     chan *JobFile
}

func NewScanner(cfg *internal.Config) *Scanner {
	return &Scanner{
		cfg:   cfg,
		jobs:  make(map[string]*JobFile),
		paths: make(map[string]*JobFile),
		C:     make(chan *JobFile),
		done:  make(chan struct{}),
	}
}

func (s *Scanner) Close() {
	close(s.done)
}

// BackupJob only move the file, not touching server.jobs or server.paths
// so, there is no need for locking
func (s *Scanner) BackupJob(jobId string, subDir string) {
	f, ok := s.jobs[jobId]
	if !ok {
		log.Info("scanner: backup job %s aborted: job not exist", jobId)
		log.Info("scanner: scanner.jobs: %+v", s.jobs)
		return
	}

	r, _ := filepath.Rel(s.cfg.JobPath, f.FilePath)
	p := filepath.Join(s.cfg.BakPath, subDir, r)
	os.MkdirAll(filepath.Dir(p), os.ModePerm)
	if e := os.Rename(f.FilePath, p); e != nil {
		log.Info("scanner: backup job %s rename err: %s", jobId, e.Error())
		copyFileContents(f.FilePath, p)
		os.Remove(f.FilePath)
	}

	pp := filepath.Dir(f.FilePath)
	pfs, _ := ioutil.ReadDir(pp)
	for pp != s.cfg.JobPath && len(pfs) == 0 {
		os.Remove(pp)
		pp = filepath.Dir(pp)
		pfs, _ = ioutil.ReadDir(pp)
	}

	log.Info("scanner: backup job %s to %s", jobId, p)
}

func (s *Scanner) scan(path string) {
	files, err := ioutil.ReadDir(path)
	if err != nil {
		log.Warning("scanner: scan path %s aborted: err %v", path, err)
		return
	}
	for _, f := range files {
		if f.IsDir() {
			dir := filepath.Join(path, f.Name())
			s.scan(dir)
		}
		if strings.HasSuffix(f.Name(), ".job") {
			n := filepath.Join(path, f.Name())
			s.readJobIfNew(n)
		}
	}
}

func (s *Scanner) Run(config *internal.Config) {
	scanTick := time.NewTicker(1 * time.Second)
	defer scanTick.Stop()
	checkJobFileTick := time.NewTicker(1 * time.Minute)
	defer checkJobFileTick.Stop()
	cleanupTick := time.NewTicker(1 * time.Hour)
	defer cleanupTick.Stop()
	for {
		select {
		case <-s.done:
			log.Warning("scanner: exit")
			return
		case <-scanTick.C:
			s.scan(config.JobPath)
		case <-checkJobFileTick.C:
			for jobId, jobFile := range s.jobs {
				if _, e := os.Stat(jobFile.FilePath); e != nil {
					delete(s.jobs, jobId)
					delete(s.paths, jobFile.FilePath)
				}
			}
		case now := <-cleanupTick.C:
			before := now.Add(time.Duration(config.KeepDays*-24) * time.Hour)
			s.cleanupDir(config.JobPath, before)
			s.cleanupDir(config.BakPath, before)
			s.cleanupDir(config.TlvPath, before)
			s.cleanupDir(config.LogPath, before)
		}
	}
}

func (s *Scanner) readJobIfNew(path string) {
	_, ok := s.paths[path]
	if ok {
		return
	}

	bytes, e := ioutil.ReadFile(path)
	if e != nil {
		log.Warning("scanner: read job file %s err: %s", path, e.Error())
		return
	}
	log.Info("scanner: read file %s job is: %s", path, string(bytes))

	j := &internal.Job{}
	e = json.Unmarshal(bytes, j)
	if e != nil {
		log.Error("scanner: decode job file %s err: %s", path, e.Error())
		return
	}

	log.Warning("scanner: read job file %s job is: %v", path, j)

	f := JobFile{
		Job:      j,
		FilePath: path,
	}

	s.jobs[j.Scene] = &f
	s.paths[path] = &f
	select {
	case <-s.done:
		log.Warning("scanner: abort read job file %s, scanner closed", path)
	case s.C <- &f:
	}

	log.Info("scanner: read job file %s success", path)
	return
}

// cleanupDir removes underlying files in path modified before specified time
func (s *Scanner) cleanupDir(path string, before time.Time) {
	log.Info("cleaning %s < %s", path, before)
	files, err := ioutil.ReadDir(path)
	if err != nil {
		log.Warning("could not read %s err: %s", path, err.Error())
		return
	}

	oldFiles := make([]os.FileInfo, 0)
	// mark old file recursively
	for _, f := range files {
		if f.ModTime().Before(before) {
			oldFiles = append(oldFiles, f)
		}
		if f.IsDir() {
			s.cleanupDir(filepath.Join(path, f.Name()), before)
		}
	}

	for _, f := range oldFiles {
		fn := filepath.Join(path, f.Name())
		childFiles, _ := ioutil.ReadDir(fn)
		if !f.IsDir() || len(childFiles) == 0 {
			log.Info("removing %s", fn)
			if e := os.RemoveAll(fn); e != nil {
				log.Warning("could not remove %s err: %s", fn, e.Error())
			}
		}
	}
}

// copyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
func copyFileContents(src, dst string) (err error) {
	in, err := os.Open(src)
	if err != nil {
		return
	}
	defer in.Close()
	out, err := os.Create(dst)
	if err != nil {
		return
	}
	defer func() {
		cerr := out.Close()
		if err == nil {
			err = cerr
		}
	}()
	if _, err = io.Copy(out, in); err != nil {
		return
	}
	err = out.Sync()
	return
}
